package com.ccn.qrcode.utils.zxing.qrcode;

import com.google.zxing.WriterException;
import com.google.zxing.common.BitArray;
import com.google.zxing.common.ECIEncoderSet;
import com.google.zxing.qrcode.decoder.ErrorCorrectionLevel;
import com.google.zxing.qrcode.decoder.Mode;
import com.google.zxing.qrcode.decoder.Version;
import com.google.zxing.qrcode.encoder.Encoder;

import java.nio.charset.Charset;
import java.util.ArrayList;
import java.util.List;

public class SelfMinimalEncoder {

    private enum VersionSize {
        SMALL("version 1-9"),
        MEDIUM("version 10-26"),
        LARGE("version 27-40");

        private final String description;

        VersionSize(String description) {
            this.description = description;
        }

        public String toString() {
            return description;
        }
    }

    private final String stringToEncode;
    private final boolean isGS1;
    private final ECIEncoderSet encoders;
    private final ErrorCorrectionLevel ecLevel;


    SelfMinimalEncoder(String stringToEncode, Charset priorityCharset, boolean isGS1, ErrorCorrectionLevel ecLevel) {
        this.stringToEncode = stringToEncode;
        this.isGS1 = isGS1;
        this.encoders = new ECIEncoderSet(stringToEncode, priorityCharset, -1);
        this.ecLevel = ecLevel;
    }

    static SelfMinimalEncoder.ResultList encode(String stringToEncode, Version version, Charset priorityCharset, boolean isGS1,
                                            ErrorCorrectionLevel ecLevel) throws WriterException {
        return new SelfMinimalEncoder(stringToEncode, priorityCharset, isGS1, ecLevel).encode(version);
    }

    ResultList encode(Version version) throws WriterException {
        if (version == null) { // compute minimal encoding trying the three version sizes.
            Version[] versions = { getVersion(VersionSize.SMALL),
                    getVersion(VersionSize.MEDIUM),
                    getVersion(VersionSize.LARGE) };
            ResultList[] results = { encodeSpecificVersion(versions[0]),
                    encodeSpecificVersion(versions[1]),
                    encodeSpecificVersion(versions[2]) };
            int smallestSize = Integer.MAX_VALUE;
            int smallestResult = -1;
            for (int i = 0; i < 3; i++) {
                int size = results[i].getSize();
                if (SelfEncoder.willFit(size, versions[i], ecLevel) && size < smallestSize) {
                    smallestSize = size;
                    smallestResult = i;
                }
            }
            if (smallestResult < 0) {
                throw new WriterException("Data too big for any version");
            }
            return results[smallestResult];
        } else { // compute minimal encoding for a given version
            ResultList result = encodeSpecificVersion(version);
            if (!SelfEncoder.willFit(result.getSize(), getVersion(getVersionSize(result.getVersion())), ecLevel)) {
                throw new WriterException("Data too big for version" + version);
            }
            return result;
        }
    }

    static SelfMinimalEncoder.VersionSize getVersionSize(Version version) {
        return version.getVersionNumber() <= 9 ? SelfMinimalEncoder.VersionSize.SMALL : version.getVersionNumber() <= 26 ?
                SelfMinimalEncoder.VersionSize.MEDIUM : SelfMinimalEncoder.VersionSize.LARGE;
    }

    static Version getVersion(SelfMinimalEncoder.VersionSize versionSize) {
        switch (versionSize) {
            case SMALL:
                return Version.getVersionForNumber(9);
            case MEDIUM:
                return Version.getVersionForNumber(26);
            case LARGE:
            default:
                return Version.getVersionForNumber(40);
        }
    }

    static boolean isNumeric(char c) {
        return c >= '0' && c <= '9';
    }

    static boolean isDoubleByteKanji(char c) {
        return SelfEncoder.isOnlyDoubleByteKanji(String.valueOf(c));
    }

    static boolean isAlphanumeric(char c) {
        return SelfEncoder.getAlphanumericCode(c) != -1;
    }

    boolean canEncode(Mode mode, char c) {
        switch (mode) {
            case KANJI: return isDoubleByteKanji(c);
            case ALPHANUMERIC: return isAlphanumeric(c);
            case NUMERIC: return isNumeric(c);
            case BYTE: return true; // any character can be encoded as byte(s). Up to the caller to manage splitting into
            // multiple bytes when String.getBytes(Charset) return more than one byte.
            default:
                return false;
        }
    }

    static int getCompactedOrdinal(Mode mode) {
        if (mode == null) {
            return 0;
        }
        switch (mode) {
            case KANJI:
                return 0;
            case ALPHANUMERIC:
                return 1;
            case NUMERIC:
                return 2;
            case BYTE:
                return 3;
            default:
                throw new IllegalStateException("Illegal mode " + mode);
        }
    }

    void addEdge(SelfMinimalEncoder.Edge[][][] edges, int position, SelfMinimalEncoder.Edge edge) {
        int vertexIndex = position + edge.characterLength;
        SelfMinimalEncoder.Edge[] modeEdges = edges[vertexIndex][edge.charsetEncoderIndex];
        int modeOrdinal = getCompactedOrdinal(edge.mode);
        if (modeEdges[modeOrdinal] == null || modeEdges[modeOrdinal].cachedTotalSize > edge.cachedTotalSize) {
            modeEdges[modeOrdinal] = edge;
        }
    }

    void addEdges(Version version, SelfMinimalEncoder.Edge[][][] edges, int from, SelfMinimalEncoder.Edge previous) {
        int start = 0;
        int end = encoders.length();
        int priorityEncoderIndex = encoders.getPriorityEncoderIndex();
        if (priorityEncoderIndex >= 0 && encoders.canEncode(stringToEncode.charAt(from),priorityEncoderIndex)) {
            start = priorityEncoderIndex;
            end = priorityEncoderIndex + 1;
        }

        for (int i = start; i < end; i++) {
            if (encoders.canEncode(stringToEncode.charAt(from), i)) {
                addEdge(edges, from, new SelfMinimalEncoder.Edge(Mode.BYTE, from, i, 1, previous, version));
            }
        }

        if (canEncode(Mode.KANJI, stringToEncode.charAt(from))) {
            addEdge(edges, from, new SelfMinimalEncoder.Edge(Mode.KANJI, from, 0, 1, previous, version));
        }

        int inputLength = stringToEncode.length();
        if (canEncode(Mode.ALPHANUMERIC, stringToEncode.charAt(from))) {
            addEdge(edges, from, new SelfMinimalEncoder.Edge(Mode.ALPHANUMERIC, from, 0, from + 1 >= inputLength ||
                    !canEncode(Mode.ALPHANUMERIC, stringToEncode.charAt(from + 1)) ? 1 : 2, previous, version));
        }

        if (canEncode(Mode.NUMERIC, stringToEncode.charAt(from))) {
            addEdge(edges, from, new SelfMinimalEncoder.Edge(Mode.NUMERIC, from, 0, from + 1 >= inputLength ||
                    !canEncode(Mode.NUMERIC, stringToEncode.charAt(from + 1)) ? 1 : from + 2 >= inputLength ||
                    !canEncode(Mode.NUMERIC, stringToEncode.charAt(from + 2)) ? 2 : 3, previous, version));
        }
    }
    SelfMinimalEncoder.ResultList encodeSpecificVersion(Version version) throws WriterException {
        int inputLength = stringToEncode.length();
        SelfMinimalEncoder.Edge[][][] edges = new SelfMinimalEncoder.Edge[inputLength + 1][encoders.length()][4];
        addEdges(version, edges, 0, null);

        for (int i = 1; i <= inputLength; i++) {
            for (int j = 0; j < encoders.length(); j++) {
                for (int k = 0; k < 4; k++) {
                    if (edges[i][j][k] != null && i < inputLength) {
                        addEdges(version, edges, i, edges[i][j][k]);
                    }
                }
            }

        }
        int minimalJ = -1;
        int minimalK = -1;
        int minimalSize = Integer.MAX_VALUE;
        for (int j = 0; j < encoders.length(); j++) {
            for (int k = 0; k < 4; k++) {
                if (edges[inputLength][j][k] != null) {
                    SelfMinimalEncoder.Edge edge = edges[inputLength][j][k];
                    if (edge.cachedTotalSize < minimalSize) {
                        minimalSize = edge.cachedTotalSize;
                        minimalJ = j;
                        minimalK = k;
                    }
                }
            }
        }
        if (minimalJ < 0) {
            throw new WriterException("Internal error: failed to encode \"" + stringToEncode + "\"");
        }
        return new SelfMinimalEncoder.ResultList(version, edges[inputLength][minimalJ][minimalK]);
    }

    private final class Edge {
        private final Mode mode;
        private final int fromPosition;
        private final int charsetEncoderIndex;
        private final int characterLength;
        private final SelfMinimalEncoder.Edge previous;
        private final int cachedTotalSize;

        private Edge(Mode mode, int fromPosition, int charsetEncoderIndex, int characterLength, SelfMinimalEncoder.Edge previous,
                     Version version) {
            this.mode = mode;
            this.fromPosition = fromPosition;
            this.charsetEncoderIndex = mode == Mode.BYTE || previous == null ? charsetEncoderIndex :
                    previous.charsetEncoderIndex; // inherit the encoding if not of type BYTE
            this.characterLength = characterLength;
            this.previous = previous;

            int size = previous != null ? previous.cachedTotalSize : 0;

            boolean needECI = mode == Mode.BYTE &&
                    (previous == null && this.charsetEncoderIndex != 0) || // at the beginning and charset is not ISO-8859-1
                    (previous != null && this.charsetEncoderIndex != previous.charsetEncoderIndex);

            if (previous == null || mode != previous.mode || needECI) {
                size += 4 + mode.getCharacterCountBits(version);
            }
            switch (mode) {
                case KANJI:
                    size += 13;
                    break;
                case ALPHANUMERIC:
                    size += characterLength == 1 ? 6 : 11;
                    break;
                case NUMERIC:
                    size += characterLength == 1 ? 4 : characterLength == 2 ? 7 : 10;
                    break;
                case BYTE:
                    size += 8 * encoders.encode(stringToEncode.substring(fromPosition, fromPosition + characterLength),
                            charsetEncoderIndex).length;
                    if (needECI) {
                        size += 4 + 8; // the ECI assignment numbers for ISO-8859-x, UTF-8 and UTF-16 are all 8 bit long
                    }
                    break;
            }
            cachedTotalSize = size;
        }
    }

    final class ResultList {

        private final List<SelfMinimalEncoder.ResultList.ResultNode> list = new ArrayList<>();
        private final Version version;

        ResultList(Version version, SelfMinimalEncoder.Edge solution) {
            int length = 0;
            SelfMinimalEncoder.Edge current = solution;
            boolean containsECI = false;

            while (current != null) {
                length += current.characterLength;
                SelfMinimalEncoder.Edge previous = current.previous;

                boolean needECI = current.mode == Mode.BYTE &&
                        (previous == null && current.charsetEncoderIndex != 0) || // at the beginning and charset is not ISO-8859-1
                        (previous != null && current.charsetEncoderIndex != previous.charsetEncoderIndex);

                if (needECI) {
                    containsECI = true;
                }

                if (previous == null || previous.mode != current.mode || needECI) {
                    list.add(0, new SelfMinimalEncoder.ResultList.ResultNode(current.mode, current.fromPosition, current.charsetEncoderIndex, length));
                    length = 0;
                }

                if (needECI) {
                    list.add(0, new SelfMinimalEncoder.ResultList.ResultNode(Mode.ECI, current.fromPosition, current.charsetEncoderIndex, 0));
                }
                current = previous;
            }

            // prepend FNC1 if needed. If the bits contain an ECI then the FNC1 must be preceeded by an ECI.
            // If there is no ECI at the beginning then we put an ECI to the default charset (ISO-8859-1)
            if (isGS1) {
                SelfMinimalEncoder.ResultList.ResultNode first = list.get(0);
                if (first != null && first.mode != Mode.ECI && containsECI) {
                    // prepend a default character set ECI
                    list.add(0, new SelfMinimalEncoder.ResultList.ResultNode(Mode.ECI, 0, 0, 0));
                }
                first = list.get(0);
                // prepend or insert a FNC1_FIRST_POSITION after the ECI (if any)
                list.add(first.mode != Mode.ECI ? 0 : 1, new SelfMinimalEncoder.ResultList.ResultNode(Mode.FNC1_FIRST_POSITION, 0, 0, 0));
            }

            // set version to smallest version into which the bits fit.
            int versionNumber = version.getVersionNumber();
            int lowerLimit;
            int upperLimit;
            switch (getVersionSize(version)) {
                case SMALL:
                    lowerLimit = 1;
                    upperLimit = 9;
                    break;
                case MEDIUM:
                    lowerLimit = 10;
                    upperLimit = 26;
                    break;
                case LARGE:
                default:
                    lowerLimit = 27;
                    upperLimit = 40;
                    break;
            }
            int size = getSize(version);
            // increase version if needed
            while (versionNumber < upperLimit && !SelfEncoder.willFit(size, Version.getVersionForNumber(versionNumber),
                    ecLevel)) {
                versionNumber++;
            }
            // shrink version if possible
            while (versionNumber > lowerLimit && SelfEncoder.willFit(size, Version.getVersionForNumber(versionNumber - 1),
                    ecLevel)) {
                versionNumber--;
            }
            this.version = Version.getVersionForNumber(versionNumber);
        }

        /**
         * returns the size in bits
         */
        int getSize() {
            return getSize(version);
        }

        private int getSize(Version version) {
            int result = 0;
            for (SelfMinimalEncoder.ResultList.ResultNode resultNode : list) {
                result += resultNode.getSize(version);
            }
            return result;
        }

        /**
         * appends the bits
         */
        void getBits(BitArray bits) throws WriterException {
            for (SelfMinimalEncoder.ResultList.ResultNode resultNode : list) {
                resultNode.getBits(bits);
            }
        }

        Version getVersion() {
            return version;
        }

        public String toString() {
            StringBuilder result = new StringBuilder();
            SelfMinimalEncoder.ResultList.ResultNode previous = null;
            for (SelfMinimalEncoder.ResultList.ResultNode current : list) {
                if (previous != null) {
                    result.append(",");
                }
                result.append(current.toString());
                previous = current;
            }
            return result.toString();
        }

        final class ResultNode {

            private final Mode mode;
            private final int fromPosition;
            private final int charsetEncoderIndex;
            private final int characterLength;

            ResultNode(Mode mode, int fromPosition, int charsetEncoderIndex, int characterLength) {
                this.mode = mode;
                this.fromPosition = fromPosition;
                this.charsetEncoderIndex = charsetEncoderIndex;
                this.characterLength = characterLength;
            }

            /**
             * returns the size in bits
             */
            private int getSize(Version version) {
                int size = 4 + mode.getCharacterCountBits(version);
                switch (mode) {
                    case KANJI:
                        size += 13 * characterLength;
                        break;
                    case ALPHANUMERIC:
                        size += (characterLength / 2) * 11;
                        size += (characterLength % 2) == 1 ? 6 : 0;
                        break;
                    case NUMERIC:
                        size += (characterLength / 3) * 10;
                        int rest = characterLength % 3;
                        size += rest == 1 ? 4 : rest == 2 ? 7 : 0;
                        break;
                    case BYTE:
                        size += 8 * getCharacterCountIndicator();
                        break;
                    case ECI:
                        size += 8; // the ECI assignment numbers for ISO-8859-x, UTF-8 and UTF-16 are all 8 bit long
                }
                return size;
            }

            /**
             * returns the length in characters according to the specification (differs from getCharacterLength() in BYTE mode
             * for multi byte encoded characters)
             */
            private int getCharacterCountIndicator() {
                return mode == Mode.BYTE ?
                        encoders.encode(stringToEncode.substring(fromPosition, fromPosition + characterLength),
                                charsetEncoderIndex).length : characterLength;
            }

            /**
             * appends the bits
             */
            private void getBits(BitArray bits) throws WriterException {
                bits.appendBits(mode.getBits(), 4);
                if (characterLength > 0) {
                    int length = getCharacterCountIndicator();
                    bits.appendBits(length, mode.getCharacterCountBits(version));
                }
                if (mode == Mode.ECI) {
                    bits.appendBits(encoders.getECIValue(charsetEncoderIndex), 8);
                } else if (characterLength > 0) {
                    // append data
                    SelfEncoder.appendBytes(stringToEncode.substring(fromPosition, fromPosition + characterLength), mode, bits,
                            encoders.getCharset(charsetEncoderIndex));
                }
            }

            public String toString() {
                StringBuilder result = new StringBuilder();
                result.append(mode).append('(');
                if (mode == Mode.ECI) {
                    result.append(encoders.getCharset(charsetEncoderIndex).displayName());
                } else {
                    result.append(makePrintable(stringToEncode.substring(fromPosition, fromPosition + characterLength)));
                }
                result.append(')');
                return result.toString();
            }

            private String makePrintable(String s) {
                StringBuilder result = new StringBuilder();
                for (int i = 0; i < s.length(); i++) {
                    if (s.charAt(i) < 32 || s.charAt(i) > 126) {
                        result.append('.');
                    } else {
                        result.append(s.charAt(i));
                    }
                }
                return result.toString();
            }
        }
    }
}
